package cn.zzdt4j.auth.filter;

import cn.hutool.json.JSONUtil;
import cn.zzdt4j.auth.model.biz.user.JwtUser;
import cn.zzdt4j.auth.model.biz.user.LoginUser;
import cn.zzdt4j.auth.toolkit.AESUtil;
import cn.zzdt4j.auth.toolkit.JwtTokenUtil;
import cn.zzdt4j.auth.toolkit.ReturnT;
import cn.zzdt4j.server.common.base.Results;
import com.fasterxml.jackson.core.exc.StreamReadException;
import com.fasterxml.jackson.databind.DatabindException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import static cn.zzdt4j.auth.constant.Constants.SPLIT_COMMA;
import static cn.zzdt4j.common.constant.Constants.BASE_PATH;
import static cn.zzdt4j.common.constant.Constants.MAP_INITIAL_CAPACITY;

/**
 * Ldap Filter.
 *
 * @author by <a href="mailto:ligang941012@gmail.com">gang.Li</a>
 * @since 2023 /10/29 23:28
 */
@Slf4j
public class LdapAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final ThreadLocal<Integer> remember = new ThreadLocal<>();

    private UserDetailsService ldapUserDetailsService;

    /**
     * Sets ldap user details service.
     *
     * @param ldapUserDetailsService the ldap user details service
     */
    public void setLdapUserDetailsService(UserDetailsService ldapUserDetailsService) {
        this.ldapUserDetailsService = ldapUserDetailsService;
    }

    /**
     * Instantiates a new Ldap authentication filter.
     *
     * @param authenticationManager the authentication manager
     */
    public LdapAuthenticationFilter(AuthenticationManager authenticationManager) {
        super.setFilterProcessesUrl(BASE_PATH + "/auth/ldap/login");
    }

    private boolean postOnly = true;

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            // Get logged in information from the input stream.
            Authentication authentication = null;
            try {
                final LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
                final String key = new StringBuffer(loginUser.getTag()).reverse().toString();
                final String password = AESUtil.decrypt(loginUser.getPassword(), key);
                loginUser.setPassword(password);
                request.setAttribute("loginUser", loginUser);
                remember.set(loginUser.getRememberMe());
                // ldap validated
                final UserDetails userDetails = ldapUserDetailsService.loadUserByUsername(loginUser.getUsername());
                authentication = new PreAuthenticatedAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            } catch (UsernameNotFoundException e) {
                log.debug("User {} not fount", e.getMessage());
                throw e;
            } catch (BadCredentialsException e) {
                log.debug("Bad credentials exception: {}", e.getMessage());
                throw e;
            } catch (Exception e) {
                log.debug("Attempt authentication error", e);
            }
            return authentication;
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        try {

            JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
            final boolean isRemember = remember.get() == 1;
            String role = "";
            final Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
            for (GrantedAuthority authority : authorities) {
                role = authority.getAuthority();
            }
            final String token = JwtTokenUtil.createToken(jwtUser.getId(), jwtUser.getUsername(), role, isRemember);
            response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
            response.setCharacterEncoding("UTF-8");
            Map<String, Object> maps = new HashMap<>(MAP_INITIAL_CAPACITY);
            maps.put("data", JwtTokenUtil.TOKEN_PREFIX + token);
            maps.put("roles", role.split(SPLIT_COMMA));
            response.getWriter().write(JSONUtil.toJsonStr(Results.success(maps)));
        } finally {
            remember.remove();
        }
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.getWriter().write(JSONUtil.toJsonStr(new ReturnT<>(ReturnT.JWT_FAIL_CODE, getMessage(failed))));
    }

    /**
     * Return different echo information to the front end according to different exception types
     *
     * @param failed failed message
     * @return recover message
     */
    private String getMessage(AuthenticationException failed) {
        String message = "Server Error";
        if (failed instanceof UsernameNotFoundException) {
            message = "用户不存在";
        } else if (failed instanceof BadCredentialsException) {
            message = "密码错误";
        }
        return message;
    }
}
